#pragma once

#include <cstddef>
#include <cstdint>
#include <stdlib.h>
#include <cstdio>
#include <cassert>
#include <type_traits>
#include <atomic>
#include <cstring>
#include <ctime>
#include <memory>
#include <iostream>
#include <fstream>
#include <map>
#include <mutex>
#include <stdexcept>
#include <functional>
#include <tuple>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <thread>
#include <cstdio> // remove file

#ifdef _WIN32
  #include <io.h>
  #include <wchar.h>
  #include <windows.h>
#endif

#include "utils/logger/default_logger.h"
#include "utils/threading/spin_lock.h"
#include "utils/timer/timer.h"
#include "utils/filesystem/filesystem.h"

#define TXDNN_STDOUT_FATAL(format, ...)                                                \
  do {                                                                                 \
    fprintf(stderr, "FATAL: " format " [" __FILE__ ":%d]\n", ##__VA_ARGS__, __LINE__); \
    fflush(stderr);                                                                    \
    assert(false);                                                                     \
  } while (0)

#define TXDNN_STDOUT_WARN(format, ...)                                                \
  do {                                                                                \
    fprintf(stderr, "WARN: " format " [" __FILE__ ":%d]\n", ##__VA_ARGS__, __LINE__); \
    fflush(stderr);                                                                   \
  } while (0)

#define TXDNN_LOG_EMERG ILogger::Level::Emergent
#define TXDNN_LOG_ALERT ILogger::Level::Alert
#define TXDNN_LOG_CRIT ILogger::Level::Critical
#define TXDNN_LOG_ERROR ILogger::Level::Error
#define TXDNN_LOG_WARN ILogger::Level::Warning
#define TXDNN_LOG_NOTICE ILogger::Level::Notice
#define TXDNN_LOG_INFO ILogger::Level::Info
#define TXDNN_LOG_DEBUG ILogger::Level::Debug
#define TXDNN_LOG_VERBOSE ILogger::Level::Verbose

#define TXDNN_LOG_DEFAULT TXDNN_LOG_DEBUG

#define TXDNN_ANY_LEVEL_TAG(L, S, SS, LOG_TAG, format, ...) \
  InternalLog(L, S, "[" SS "][" LOG_TAG ":" format "][" __FILE__ ":%d]", ##__VA_ARGS__, __LINE__);

#define TXDNN_ANY_LEVEL(S, SS, format, ...) TXDNN_ANY_LEVEL_TAG(logger_, S, SS, LOG_TAG, format, ##__VA_ARGS__)

#define TXLOG_EMERG(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_EMERG, "EMERG", format, ##__VA_ARGS__)
#define TXLOG_ALERT(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_ALERT, "ALERT", format, ##__VA_ARGS__)
#define TXLOG_CRIT(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_CRIT, "CRIT", format, ##__VA_ARGS__)
#define TXLOG_ERROR(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_ERROR, "ERROR", format, ##__VA_ARGS__)
#define TXLOG_WARN(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_WARN, "WARN", format, ##__VA_ARGS__)
#define TXLOG_NOTICE(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_NOTICE, "NOTICE", format, ##__VA_ARGS__)
#define TXLOG_INFO(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_INFO, "INFO", format, ##__VA_ARGS__)
#define TXLOG_DEBUG(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_DEBUG, "DEBUG", format, ##__VA_ARGS__)
#define TXLOG_VERBOSE(format, ...) TXDNN_ANY_LEVEL(TXDNN_LOG_VERBOSE, "VERBOSE", format, ##__VA_ARGS__)

namespace txdnn {
/*
 * Logger
 */
class Logger : public ILogger {
public:
  explicit Logger(Level level) _TX_NOEXCEPT : level_(level) {}
  ~Logger() _TX_NOEXCEPT = default;

  void log(ILogger::Level level, const char* thread_local_buffer) _TX_NOEXCEPT _TX_OVERRIDE {
    if (!isLogLevelEnabled(level) or !thread_local_buffer) {
      return;
    }
    auto current_time = std::chrono::high_resolution_clock::now();
    // multi-thread lock
    SpinLockGuard _(lock_);
    PrintTimeToBuffer(time_msg_, current_time);
    _writeMsg(thread_local_buffer);
  }

  bool isLogLevelEnabled(Level level) _TX_NOEXCEPT _TX_OVERRIDE { return (level <= level_); }

private:
  virtual void _writeMsg(const char* msg) _TX_NOEXCEPT = 0;

protected:
  Level level_ = Level::Invalid;
  SpinLock lock_;
  char time_msg_[k_time_len + 1] = {0};
};

/*
 * StdLogger
 */
class StdLogger : public Logger {
public:
  using Logger::Logger;
  ~StdLogger() _TX_NOEXCEPT = default;

private:
  void _writeMsg(const char* msg) _TX_NOEXCEPT _TX_OVERRIDE { printf("%s %s\n", time_msg_, msg); }
};

class IFlusher : private NonCopyable {
public:
  virtual ~IFlusher() _TX_NOEXCEPT = default;
  virtual void write(const char* time_msg, const char* msg) _TX_NOEXCEPT = 0;
  virtual void flush() _TX_NOEXCEPT = 0;
};

/*
 * FileFlusher
 */
class FileFlusher _TX_FINAL : public IFlusher {
public:
  explicit FileFlusher(const std::string& file_name) {
    if (file_name.empty()) {
      TXDNN_STDOUT_FATAL("Empty file name in file flusher");
      return;
    }
    out_stream_.open(file_name, std::ios::ate | std::ios::app);
    if (!(file_opened_ = out_stream_.is_open())) {
      TXDNN_STDOUT_WARN("Failed to open log file: %s", file_name.c_str());
      return;
    }
    out_stream_.setf(std::ios::unitbuf);
  }

  virtual ~FileFlusher() { flush(); }

  void write(const char* time_msg, const char* msg) _TX_NOEXCEPT _TX_OVERRIDE {
    if (file_opened_) {
      out_stream_ << time_msg << " " << msg << std::endl;
    }
  }

  void flush() _TX_NOEXCEPT _TX_OVERRIDE {
    if (file_opened_) {
      out_stream_.flush();
    }
  }

private:
  std::ofstream out_stream_;
  bool file_opened_ = false;
};

/*
 * FileFLogger
 */
class FileLogger : public Logger {
public:
  FileLogger(Level level, std::unique_ptr<FileFlusher>&& flusher) : Logger(level), flusher_(std::move(flusher)) {}
  ~FileLogger() _TX_NOEXCEPT = default;

private:
  void _writeMsg(const char* msg) _TX_NOEXCEPT _TX_OVERRIDE {
    flusher_->write(time_msg_, msg);
    flusher_->flush();
  }

private:
  std::unique_ptr<FileFlusher> flusher_;
};

// multi-thread calling this func should have multiple buffers

class LoggerBuilder _TX_FINAL {
public:
  ~LoggerBuilder() _TX_NOEXCEPT { free(managed_buffer_); }

  template<typename... Args>
  const char* build(const char* format, Args... args) {
#ifdef DYNAMIC_SIZE_LOGGER
    auto length = std::snprintf(nullptr, 0, format, args...);
    if (length <= 0) {
      return "";
    }
    _setManaged(length + 1);
#else
    _setUnmanaged();
#endif // DYNAMIC_SIZE_LOGGER

    std::snprintf(buffer_, size_, format, args...);
    return buffer_;
  }

private:
  void _setManaged(std::size_t size) _TX_NOEXCEPT {
    if (buffer_ == managed_buffer_ && size_ == size) {
      return;
    }
    free(managed_buffer_);
    managed_buffer_ = static_cast<char*>(malloc(size));
    if (!managed_buffer_) {
      return;
    }
    buffer_ = managed_buffer_;
    size_ = size;
  }

  void _setUnmanaged() _TX_NOEXCEPT {
    buffer_ = k_buf;
    size_ = k_max_char_one_log;
  }

public:
  static constexpr std::size_t k_max_char_one_log = 512;
  thread_local static inline char k_buf[k_max_char_one_log];

private:
  char* managed_buffer_ = nullptr;
  char* buffer_ = nullptr;
  std::size_t size_ = 0;
};

template<typename... Args>
void InternalLog(ILogger* logger, ILogger::Level level, const char* format, Args... args) {
  if (!logger) {
    return;
  }
  logger->log(level, LoggerBuilder().build(format, args...));
}


/* File Event Handler */
struct FileEventHandler {
  FileEventHandler() {}

  std::function<void(const std::string& file_name)> before_open;
  std::function<void(const std::string& file_name, std::FILE* file_stream)> after_open;
  std::function<void(const std::string& file_name, std::FILE* file_stream)> before_close;
  std::function<void(const std::string& file_name)> after_close;
};

/* Helper class for log file 
 * When failed to open a log file, retry servel times (5) with delay 10ms
 */
class FileManipulateHelper {
public:
  FileManipulateHelper() = default;
  explicit FileManipulateHelper(const FileEventHandler& handlers) : handlers_(handlers) {}
  ~FileManipulateHelper() { close(); }

  FileManipulateHelper(const FileManipulateHelper&)             = delete;
  FileManipulateHelper &operator=(const FileManipulateHelper&)  = delete;

  void open(const std::string& fname, bool truncate = false) {
    close();
    file_name_ = fname;

    auto* mode = "ab";
    auto* trunc_mode = "wb";

    if (handlers_.before_open) {
      handlers_.before_open(file_name_);
    }

    for (int tries = 0; tries < open_tries_; ++tries) {
      CreateDir(DirName(fname));
      if (truncate) {
        // opening-and-closing a tmp file in "wb" mode to truncate;
        // opening actual log in "ab" mode, if external processes may rotate/truncate file in micro-second
        FILE* tmp;
        if (FileOpen(&tmp, fname, trunc_mode)) { continue; }
        fclose(tmp);
      }
      if (!FileOpen(&fd_, fname, mode)) {
        if (handlers_.after_open) {
          handlers_.after_open(file_name_, fd_);
        }
        return;
      }
      SleepMillis(open_interval_);
    }
    std::cout << "Failed to open file " + file_name_ + " for writing" << std::endl;
    std::abort();
  }

  void reopen(bool truncate) {
    if (file_name_.empty()) {
      std::cout << "Failed to re-open file as not open before" << std::endl;
      std::abort();
    }
    this->open(file_name_, truncate);
  }

  void flush() {
    if (std::fflush(fd_)) {
      std::cout << "Failed to flush file " + file_name_ << std::endl;
      std::abort();
    }
  }

  void sync() {
    if (!FileSync(fd_)) {
      std::cout << "Failed to sync file " + file_name_ << std::endl;
      std::abort();
    }
  }

  void close() {
    if (fd_ != nullptr) {
      if (handlers_.before_close) {
        handlers_.before_close(file_name_, fd_);
      }

      std::fclose(fd_);
      fd_ = nullptr;

      if (handlers_.after_close) {
        handlers_.after_close(file_name_);
      }
    }
  }

  void write(const std::string& buffer) {
    if (fd_ == nullptr) { return; }
    size_t msg_size = buffer.size();
    auto data = buffer.data();

    if (!WriteBytes(data, msg_size, fd_)) {
      std::cout << "Failed to write file " + file_name_ << std::endl;
      std::abort();
    }
  }

  std::size_t size() const {
    if (fd_ == nullptr) {
      std::cout << "size(): file closed " + file_name_ << std::endl;
      std::abort(); 
    }
    return FileSize(fd_);
  }

  const std::string& fname() const { return file_name_; }

  // return path and extension
  // "logger.log"           -> ("logger", ".log")
  // "logger"               -> ("logger", "")
  // "logger."              -> ("logger.", "")
  // "dir1/dir2/logger.log" -> ("dir1/dir2/logger", ".log")
  //
  // hidden files: starting dot in file name is ignored
  // "dir/.txdnnlog"        -> ("dir/.txdnnlog", "")
  // "dir/.txdnnlog.log"    -> ("dir/.txdnnlog", ".log")
  static std::tuple<std::string, std::string> splitExt(const std::string& fname) {
    auto ext_idx = fname.rfind('.');
    // no valid extension found - return path and empty string as ext
    if (ext_idx == std::string::npos || ext_idx == 0 || ext_idx == fname.size() - 1) {
      return std::make_tuple(fname, std::string());
    }

    // case: "/etc/rc.d/logfile" or "/folder/.hidden_file"
    auto folder_idx = fname.find_last_of(SEP);
    if (folder_idx != std::string::npos && folder_idx >= ext_idx - 1) {
      return std::make_tuple(fname, std::string());
    }

    return std::make_tuple(fname.substr(0, ext_idx), fname.substr(ext_idx));
  }

private:
  const int open_tries_ = 5;
  const unsigned int open_interval_ = 10;
  std::FILE* fd_{nullptr};
  std::string file_name_;
  FileEventHandler handlers_;
};

/* Rotating FileLog Base */
template <typename Mutex>
class RotatingBase {
public:
  RotatingBase();
  ~RotatingBase() _TX_OVERRIDE = default;

protected:
  Mutex mutex_;
};

/* Rotating File */
template <typename Mutex>
class RotatingFile _TX_FINAL : public RotatingBase<Mutex> {
public:
  RotatingFile(std::string file_name,
              std::size_t max_size,
              std::size_t max_files,
              bool rotate_on_open,
              const FileEventHandler& handlers = {})
    : base_filename_(std::move(file_name)), max_size_(max_size), max_files_(max_files), file_helper_{handlers} {
    if (max_size == 0) {
      throw std::logic_error("max_size cannot be zero");
    }
    if (max_files > 10000) {
      throw std::logic_error("max_files cannot exceed 10000");
    }

    file_helper_.open(rename(base_filename_, 0));
    current_size_ = file_helper_.size();
    if (rotate_on_open && current_size_ > 0) {
      _rotate();
      current_size_ = 0;
    }
  }

  static std::string rename(const std::string& fname, std::size_t idx) {
    if (idx == 0u) {
      return fname;
    }

    std::string base_name, ext;
    std::tie(base_name, ext) = FileManipulateHelper::splitExt(fname);
    return std::string{base_name + std::to_string(idx) + ext};
  }

  std::string fname() {
    std::lock_guard<Mutex> lock(RotatingBase<Mutex>::mutex_);
    return file_helper_.fname();
  }

  void rotate() {
    std::lock_guard<Mutex> lock(RotatingBase<Mutex>::mutex_);
    _rotate();
  }

private:
  // Roll files:
  // logger.log   -> logger.1.log
  // logger.1.log -> logger.2.log
  // logger.2.log -> logger.3.log
  // logger.3.log -> delete
  void _rotate() {
    file_helper_.close();

    for (auto i = max_files_; i > 0; --i) {
      std::string src = rename(base_filename_, i - 1);
      if (!PathExists(src)) {
        continue;
      }
      std::string dst = rename(base_filename_, i);

      if (!_rename(src, dst)) {
        // if failed, try again after a small delay.
        // if very high rotating rate, it causes rename to fail with permission denied.
        SleepMillis(100);
        if (!_rename(src, dst)) {
          file_helper_.reopen(true); // truncate log file to prevent from growing beyond limit
          current_size_ = 0;
          std::cout << "_rotate() : failed renameing " + src + " to " + dst + "\n";
          std::abort(); 
        }
      }
    }

    file_helper_.reopen(true);
  }

  bool _rename(const std::string& src, const std::string& dst) {
    (void)RemoveFile(dst);
    return RenameFile(src, dst) == 0;
  }

  std::string base_filename_;
  std::size_t max_size_;
  std::size_t max_files_;
  std::size_t current_size_;
  FileManipulateHelper file_helper_;
};

using rotating_file_mt = RotatingFile<std::mutex>;

} // namespace txdnn
